/**
 @license
 Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
 This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
 The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
 Code distributed by Google as part of the polymer project is also
 subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
 */
import './async.js';

/**
 * @summary Collapse multiple callbacks into one invocation after a timer.
 */
export class Debouncer {
    constructor() {
        this._asyncModule = null;
        this._callback = null;
        this._timer = null;
    }

    /**
     * Sets the scheduler; that is, a module with the Async interface,
     * a callback and optional arguments to be passed to the run function
     * from the async module.
     *
     * @param {!AsyncInterface} asyncModule Object with Async interface.
     * @param {function()} callback Callback to run.
     * @return {void}
     */
    setConfig(asyncModule, callback) {
        this._asyncModule = asyncModule;
        this._callback = callback;
        this._timer = this._asyncModule.run(() => {
            this._timer = null;
            debouncerQueue.delete(this);
            this._callback();
        });
    }

    /**
     * Cancels an active debouncer and returns a reference to itself.
     *
     * @return {void}
     */
    cancel() {
        if (this.isActive()) {
            this._cancelAsync();
            // Canceling a debouncer removes its spot from the flush queue,
            // so if a debouncer is manually canceled and re-debounced, it
            // will reset its flush order (this is a very minor difference from 1.x)
            // Re-debouncing via the `debounce` API retains the 1.x FIFO flush order
            debouncerQueue.delete(this);
        }
    }

    /**
     * Cancels a debouncer's async callback.
     *
     * @return {void}
     */
    _cancelAsync() {
        if (this.isActive()) {
            this._asyncModule.cancel(/** @type {number} */(this._timer));
            this._timer = null;
        }
    }

    /**
     * Flushes an active debouncer and returns a reference to itself.
     *
     * @return {void}
     */
    flush() {
        if (this.isActive()) {
            this.cancel();
            this._callback();
        }
    }

    /**
     * Returns true if the debouncer is active.
     *
     * @return {boolean} True if active.
     */
    isActive() {
        return this._timer != null;
    }

    /**
     * Creates a debouncer if no debouncer is passed as a parameter
     * or it cancels an active debouncer otherwise. The following
     * example shows how a debouncer can be called multiple times within a
     * microtask and "debounced" such that the provided callback function is
     * called once. Add this method to a custom element:
     *
     * ```js
     * import {microTask} from '@polymer/polymer/lib/utils/async.js';
     * import {Debouncer} from '@polymer/polymer/lib/utils/debounce.js';
     * // ...
     *
     * _debounceWork() {
     *   this._debounceJob = Debouncer.debounce(this._debounceJob,
     *       microTask, () => this._doWork());
     * }
     * ```
     *
     * If the `_debounceWork` method is called multiple times within the same
     * microtask, the `_doWork` function will be called only once at the next
     * microtask checkpoint.
     *
     * Note: In testing it is often convenient to avoid asynchrony. To accomplish
     * this with a debouncer, you can use `enqueueDebouncer` and
     * `flush`. For example, extend the above example by adding
     * `enqueueDebouncer(this._debounceJob)` at the end of the
     * `_debounceWork` method. Then in a test, call `flush` to ensure
     * the debouncer has completed.
     *
     * @param {Debouncer?} debouncer Debouncer object.
     * @param {!AsyncInterface} asyncModule Object with Async interface
     * @param {function()} callback Callback to run.
     * @return {!Debouncer} Returns a debouncer object.
     */
    static debounce(debouncer, asyncModule, callback) {
        if (debouncer instanceof Debouncer) {
            // Cancel the async callback, but leave in debouncerQueue if it was
            // enqueued, to maintain 1.x flush order
            debouncer._cancelAsync();
        } else {
            debouncer = new Debouncer();
        }
        debouncer.setConfig(asyncModule, callback);
        return debouncer;
    }
}

let debouncerQueue = new Set();

/**
 * Adds a `Debouncer` to a list of globally flushable tasks.
 *
 * @param {!Debouncer} debouncer Debouncer to enqueue
 * @return {void}
 */
export const enqueueDebouncer = function (debouncer) {
    debouncerQueue.add(debouncer);
};

/**
 * Flushes any enqueued debouncers
 *
 * @return {boolean} Returns whether any debouncers were flushed
 */
export const flushDebouncers = function () {
    const didFlush = Boolean(debouncerQueue.size);
    // If new debouncers are added while flushing, Set.forEach will ensure
    // newly added ones are also flushed
    debouncerQueue.forEach(debouncer => {
        try {
            debouncer.flush();
        } catch (e) {
            setTimeout(() => {
                throw e;
            });
        }
    });
    return didFlush;
};
